#!/usr/bin/env python

# (c) Copyright 2008 Paul Hankin. All Rights Reserved. 

__import__('sys').path.append('..')

import unittest
from bg.enumerate import enumerate_moves
import bg.main as bg

class PipCountTestCase(unittest.TestCase):
    def testInitialPip(self):
        "Check the starting position has pipcount of 167 for both players"
        b = bg.initialPosition
        self.assertEquals(b.pipcounts, (167, 167))
        self.assertEquals(b.reverse().pipcounts, (167, 167))
    def testPipAfterMoves(self):
        "From initial position, make sure every move has the right pipcount."
        b = bg.initialPosition
        pc0, pc1 = b.pipcounts
        for d0, d1 in bg.all_rolls():
            pip_diff = d0 + d1 if d0 != d1 else d0 * 4
            count = 0
            for moves, board in enumerate_moves(b, d0, d1):
                assert isinstance(board, bg.Board)
                assert isinstance(moves, bg.Move)
                self.assertEquals(board.pipcounts, (pc0 - pip_diff, pc1))
                # Check reversed board has pipcounts the other way round
                board_rev = board.reverse()
                self.assertEquals(board_rev.pipcounts, (pc1, pc0 - pip_diff))
                # Check move thinks final board is correct
                self.assertEquals(moves.board_after, board)
                # And check every move can be converted from string back
                # to move.
                move_string = str(moves)
                move = bg.Move.from_string(b, move_string)
                self.assertEquals(move_string, str(move))
                count += 1
            # There should be at least 4 moves for each roll.
            # (55 has exactly 4 rolls -- see testOpening55)
            assert count >= 4
            if d0 == d1 == 5: assert count == 4

class MoveEnumeratorTestCase(unittest.TestCase):
    def testSomeMovesAvailable(self):
        "Check moves are available from the initial position for all rolls"
        b = bg.initialPosition
        for d0, d1 in bg.all_rolls():
            self.assertNotEquals(list(enumerate_moves(b, d0, d1)), [])
    def _test_moves(self, moves, correct_moves):
        "Check that a list of moves matches a given list of moves"
        moves = list(moves)
        mvs_text = [str(m) for m, b in moves]
        move_list = ', '.join(map(repr, mvs_text))
        for m in mvs_text:
            self.failIf(m not in correct_moves,
                msg='Extra move %s in [%s]' % (m, move_list))
        for m in correct_moves:
            self.failIf(m not in mvs_text,
                msg='Missing move %s in [%s]' % (m, move_list))
        self.assertEquals(sorted(mvs_text), sorted(correct_moves))
        # Check that converting the moves back from text and back to a
        # move preserves the (text of the) move.
        moves_back = [bg.Move.from_string(m._board, str(m)) for m, b in moves]
        self.assertEquals(len(moves_back), len(mvs_text))
        for m, t in zip(moves_back, mvs_text):
            self.assertEquals(str(m), t)
        # Check move calculates post-move board correctly
        for m, b in moves:
            self.assertEquals(b, m.board_after)
    def testOpening31(self):
        "Check all opening 31's from the initial position"
        all_moves = [
            '24/21 24/23', '24/20', '24/21 8/7', '24/21 6/5',
            '24/23 13/10', '24/23 8/5', '24/23 6/3',
            '13/9', '13/10 8/7', '13/10 6/5',
            '8/5 8/7', '8/4', '8/5 6/5',
            '8/7 6/3',
            '6/3 6/5', '6/2'
            ]
        self._test_moves(enumerate_moves(bg.initialPosition, 3, 1),
            all_moves)
    def testOpening66(self):
        "Check all opening 66's"
        all_moves = [
            '24/18(2) 13/7(2)', '24/18(2) 13/7 8/2', '24/18(2) 8/2(2)',
            '24/18 13/7(3)', '24/18 13/7(2) 8/2', '24/18 13/7 8/2(2)', '24/18 8/2(3)',
            '13/7(4)', '13/7(3) 8/2', '13/7(2) 8/2(2)', '13/7 8/2(3)'
        ]
        self._test_moves(enumerate_moves(bg.initialPosition, 6, 6),
            all_moves)
    def testOpening55(self):
        "Check all opening 55's"
        all_moves = ['13/8(4)', '13/3 13/8(2)', '13/3(2)', '13/3 8/3(2)']
        self._test_moves(enumerate_moves(bg.initialPosition, 5, 5),
            all_moves)
    def testOptionalHit(self):
        "Check optional hit is reported with and without hit."
        board = bg.Board.from_points((6, 1), (5, -1))
        moves = ['6/5*/3', '6/3']
        self._test_moves(enumerate_moves(board, 2, 1), moves)
    def testDifferentHits(self):
        "Check different hits are reported separately."
        board = bg.Board.from_points((6, 1), (5, -1), (4, -1))
        moves = ['6/5*/3', '6/4*/3']
        self._test_moves(enumerate_moves(board, 2, 1), moves)
    def testOnBar(self):
        "Check man on bar can't come on whatever the roll."
        board = bg.Board.from_points((6, 5), (25, 1), *((-i, -2) for i in xrange(1, 7)))
        for d0, d1 in bg.all_rolls():
            self._test_moves(enumerate_moves(board, d0, d1), [])
    def testBarMinorAndOn(self):
        "Bring a man on (with the small die) and move him."
        board = bg.Board.from_points((-6, -2), (25, 1))
        moves = ['bar/14']
        self._test_moves(enumerate_moves(board, 6, 5), moves)
    def testBarMajorAndOn(self):
        "Bring a man on (with the big die) and move him."
        board = bg.Board.from_points((-5, -2), (25, 1))
        moves = ['bar/14']
        self._test_moves(enumerate_moves(board, 6, 5), moves)
    def testBarAndMove(self):
        "Test come in and play the other die."
        board = bg.Board.from_points((25, 1), (13, 1))
        moves = ['bar/22', 'bar/23 13/12', 'bar/24 13/11']
        self._test_moves(enumerate_moves(board, 2, 1), moves)
    def testComeBiOn(self):
        "Test we can bear two men on."
        board = bg.Board.from_points((25, 2))
        moves = ['bar/19 bar/20']
        self._test_moves(enumerate_moves(board, 6, 5), moves)
    def testComeQuadriOn(self):
        "Test we can bear four men on with doublet."
        board = bg.Board.from_points((25, 4))
        moves = ['bar/20(4)']
        self._test_moves(enumerate_moves(board, 5, 5), moves)
    def testComeTriOnAndMove(self):
        "Test we can bear three men on and move with doublet."
        board = bg.Board.from_points((25, 3))
        moves = ['bar/15 bar/20(2)']
        self._test_moves(enumerate_moves(board, 5, 5), moves)
    def testDoubleFiveFromBar(self):
        "Test we can move to our 5 point from bar with 55."
        board = bg.Board.from_points((25, 1))
        moves = ['bar/5']
        self._test_moves(enumerate_moves(board, 5, 5), moves)
    def testBearOffSameWithDoublet(self):
        "Move two men in and bear them off"
        board = bg.Board.from_points((12, 2))
        moves = ['12/off(2)']
        self._test_moves(enumerate_moves(board, 6, 6), moves)
    def testBearOffSameWithHitWithDoublet(self):
        "Move two men in and bear them off"
        board = bg.Board.from_points((12, 2), (6, -1))
        moves = ['12/6*/off(2)']
        self._test_moves(enumerate_moves(board, 6, 6), moves)
    def testBearOffSameWithHitWithDoubletVariant(self):
        "Move three men in and bear them off (with hit)"
        board = bg.Board.from_points((12, 3), (6, -1))
        moves = ['12/6*/off 12/6(2)']
        self._test_moves(enumerate_moves(board, 6, 6), moves)
    def testBearOffSameWithDoubletVariant(self):
        "Move three men in, bearing one off"
        board = bg.Board.from_points((12, 3))
        moves = ['12/off 12/6(2)']
        self._test_moves(enumerate_moves(board, 6, 6), moves)
    def testBearOffSameWithHitDoubletAllHome(self):
        "Move choice three men with hit"
        board = bg.Board.from_points((6, 3), (3, -1))
        moves = ['6/3*/off 6/3(2)', '6/3*/off(2)']
        self._test_moves(enumerate_moves(board, 3, 3), moves)
    def testBearOffSameWithHitDoubletAllHome2(self):
        "Move choice three men without hit"
        board = bg.Board.from_points((6, 3))
        moves = ['6/off 6/3(2)', '6/off(2)']
        self._test_moves(enumerate_moves(board, 3, 3), moves)
    def testBearOff4321(self):
        "Check we can eat the last 1, 2, 3 or 4 men with a double"
        for i in xrange(1, 5):
            board = bg.Board.from_points((6, i))
            moves = ['6/off%s' % ('' if i == 1 else '(%d)' % i)]
            self._test_moves(enumerate_moves(board, 6, 6), moves)
    def testBigDiceOnly(self):
        "Check if we have a choice of which dice to use, we use the big one."
        board = bg.Board.from_points((6, 1), (3, -2), (2, -2))
        moves = ['6/4']
        self._test_moves(enumerate_moves(board, 2, 1), moves)
    def testBiOff(self):
        "Check we can bear two men off"
        board = bg.Board.from_points((6, 1), (5, 1))
        moves = ['6/off 5/off', '6/1 5/off']
        self._test_moves(enumerate_moves(board, 6, 5), moves)
    def testInexactMajor(self):
        "Check we can't eat a man inexactly when there's a higher point"
        board = bg.Board.from_points((5, 3), (3, 1))
        moves = ['5/off 5/1']
        self._test_moves(enumerate_moves(board, 6, 4), moves)
    def testInAndEat(self):
        "Check we can move a man in, and eat him"
        board = bg.Board.from_points((7, 1))
        moves = ['7/off']
        self._test_moves(enumerate_moves(board, 6, 1), moves)

class DiceTestCase(unittest.TestCase):
    def testRollCount(self):
        "Check there are the right number of rolls"
        self.assertEqual(len(list(bg.all_rolls())), 21)
    def testAllRollsGood(self):
        "Check all rolls are in the range 1..6"
        good = xrange(1, 7)
        self.failIf(any(d0 not in good for d0, d1 in bg.all_rolls()))
        self.failIf(any(d1 not in good for d0, d1 in bg.all_rolls()))
    def testAllRollsExist(self):
        "Check all rolls are in the list"
        for d0 in xrange(1, 7):
            for d1 in xrange(1, 7):
                self.failIf((max(d0, d1), min(d0, d1)) not in bg.all_rolls())
    def testAllRollsDecreasing(self):
        "Check big dice comes first for all rolls."
        self.failIf(any(d0 < d1 for d0, d1 in bg.all_rolls()))

class PositionIDTestCase(unittest.TestCase):
    def testKnownPositions(self):
        "Check position IDs for some positions."
        examples = [
            # Starting position
            ("4HPwATDgc/ABMA", bg.initialPosition),
            # Midgame position with a man on the bar
            ("yOfgATCwt+ABUA", bg.Board.from_points(
                (-25, -1), (-24, -1), (-13, -4), (-8, -1),
                (-7, -2), (-6, -4), (-5, -2),
                (-21, 1), (-19, 5), (-17, 3), (-12, 4), (-1, 2))),
            # A bearoff position with both players having some men off
            ("ujYAALwuAwAAAA", bg.Board.from_points(
                (-6, -2), (-4, -1), (-3, -3), (-2, -1), (-1, -4),
                (-19, 2), (-20, 2), (-21, 1), (-22, 3), (-23, 1)
            )),
        ]
        for pid, board in examples:
            try:
                # Does the board have the right position ID?
                self.assertEqual(board.positionID, pid)
                # Does the position ID generate the right board?
                self.assertEqual(board, bg.Board.from_positionID(pid))
                # pid -> board -> pid is identity function
                self.assertEqual(pid, bg.Board.from_positionID(pid).positionID)
                # board -> pid -> board is identity function
                self.assertEqual(board, bg.Board.from_positionID(board.positionID))
            except AssertionError:
                print board
                print bg.Board.from_positionID(pid)
                raise

if __name__ == '__main__':
    unittest.main()
